import json
import os
import requests
import datetime
import threading
import math
import logging

from .GroupSolver import GroupSolver
from .AreaSolver import AreaSolver
from .ScheduleSolver import ScheduleSolver
from .util.json2graph import json2graph
from .base_class import Area
from .parse_schedule_data import parse_schedule_data, schedule_data_fetch_check, parse_existing_workdata
from .parse_data.parse_flight import json_to_flight, parse_task_data
from .util import message
from .util.fetch_data import get_worktime_by_range, get_area_data
from .util.error import GeneralErr, UNKNOWN_ERROR


def scheduling(request, is_nextday, is_data_ol):
    """
    排班主方法。
    is_nextday 字段区分是否为次日排班。
    包含日志相关代码。
    """

    # 数据初始化
    time_now = datetime.datetime.now()
    time_now_string = time_now.strftime("%Y-%m-%d_%H-%M-%S")

    # 打开记录日志
    data_path, log_path = get_cache_path(request.remote_addr)
    log_file = open(log_path,'a+')
    log_file.write(time_now_string + '\t' + '收到POST请求：' + request.remote_addr + '\n')

    # 保存从凯亚收到的接口调用数据
    with open(data_path+'request'+time_now_string+'.json','w+',encoding='utf-8') as request_data_file:
        request_data_file.write(str(request.data,encoding='utf-8'))

    # 解析数据
    data, err_msg = data_preparation(request, is_data_ol)
    if err_msg:
        raise GeneralErr(
            code = err_msg["resultCode"],
            msg = err_msg["resultMessage"]
        )

    # 多线程, 异步计算排班结果
    callback_url = data["arrangeData"]["callBackUrl"]
    message_id = data["arrangeData"]["messageId"]
    schedule_demands = data["schedule_demands"]
    flight_dto = data["flight_dto"]

    # # 测试环境
    # result = None
    # result = schedule_alg(
    #     schedule_demands=schedule_demands,
    #     message_id=message_id,
    #     is_nextday=is_nextday,
    #     flight_dto=flight_dto
    # )
    
    # 发布环境 （上线版本入参不传入result）
    t = threading.Thread(
        name = "Scheduling NextDay Thread",
        target = run_scheduling,
        kwargs = {
            "schedule_demands": schedule_demands,
            "message_id": message_id,
            "is_nextday": is_nextday,
            "flight_dto": flight_dto,
            "callback_url": callback_url,
            # "result": result  
        })
    t.start()

    # # 保存返回数据
    # if result:
    #     result_file = json.dumps(result)
    #     with open(data_path+'return'+time_now_string+'.json','w+',encoding='utf-8') as return_data_file:
    #         return_data_file.write(result_file)

    # 保存信息到log文件    
    log_file.write(time_now_string + '\t' + '返回排班结果' + '\n')
    log_file.close()
    
    # 返回信息
    return {"data": message.SUCCESS, "err": None}


def get_cache_path(ip):
    """
    请求信息和结果缓存地址处理
    """

    # 初始化缓存文件信息
    BASE_DATA_PATH = os.path.join(os.path.dirname(__file__), os.path.pardir) + '/cache/'
    data_path = BASE_DATA_PATH
    if ip == "112.6.126.153":
        data_path += 'kaiya/'
    else:
        data_path += 'guoyi/'

    if not os.path.exists(data_path):
        os.mkdir(data_path)
    log_file_name = 'log.txt'

    return data_path, data_path+log_file_name


def data_preparation(request, is_data_ol):
    """
    数据处理主方法。
    获取班制数据、获取历史数据、获取部门区域机位数据等
    解析请求数据（部门、班制、航班、当日排班信息等）
    数据格式检查、数据关联性检查
    """

    ip = request.remote_addr
    request_data = request.data
    
    # 初始化函数内部变量
    json_request_data = json.loads(request_data)
    schedule_dt = datetime.datetime.strptime(json_request_data["scopeStart"], "%Y-%m-%d")

    # 开始解析
    # 数据检查（只筛选请求排班的部门的班制）（获取班制数据）
    arrangeData, err_msg = schedule_data_fetch_check(
        json_request_data, ip, is_data_ol)

    if not arrangeData:
        print(err_msg)
        return None, err_msg

    # 解析凯亚数据(无数据获取)
    schedule_demands, err_msg = parse_schedule_data(arrangeData)
    if not schedule_demands:
        print(err_msg)
        return None, err_msg

    # 获取排班日历史数据（次日排班使用）
    nextday_data, err_msg = get_worktime_by_range(
        str(schedule_dt), str(schedule_dt), is_data_ol)
    if err_msg:
        return None, err_msg

    # 解析航班数据(无数据获取)
    flight_dto, err_msg = json_to_flight(json_request_data["flightDto"])

    if type(err_msg) == list:
        print("===Flight Without Stand===") # 无机位航班
        print(f"Count: {len(err_msg)}")
        for f in err_msg:
            print(f)
        print()
    else:
        return None, err_msg

    # 组合排班算法输入
    for unit in schedule_demands["schedule_list"]:
        # 筛选排班日历史数据
        arrange_data = []
        if nextday_data:
            mainshift_worktime_ids = [wt.id for wt in unit.main_shift.worktime_list]
            arrange_data =  [arr for arr in nextday_data \
                if (arr["workTimeId"] in mainshift_worktime_ids)]
            unit.arrange_data = arrange_data

            if unit.aux_shift:
                auxshift_worktime_ids = [wt.id for wt in unit.aux_shift.worktime_list]
                overtime_data =  [arr for arr in nextday_data \
                    if (arr["workTimeId"] in auxshift_worktime_ids)]
                unit.overtime_data = overtime_data
            else:
                unit.overtime_data = []

        # 获取和解析部门、区域、机位关系数据
        area_data, areas = get_area_data(unit.department.id, Area, is_data_ol)
        unit.areas = areas

        # 解析航班在部门区域内的分布
        # TEST:　还需完善，要根据班型的上班时段解析航班分布
        area_flight_info = {}
        stand_no_area_list = set()
        for fli in flight_dto:
            try:
                area = area_data["craftSeatDtos"][fli._remote_stand.name]
                if area_flight_info.__contains__(area.id):
                    area_flight_info[area.id].append(fli)
                else:
                    area_flight_info[area.id] = [fli]
            except:
                stand_no_area_list.add(str(fli._remote_stand))
        unit.area_data = area_flight_info

        print("===Stand Without Area===")
        print(f"Count: {len(stand_no_area_list)}")
        for s in stand_no_area_list:
            print(s)
        print()

        # 获取当月历史数据
        if schedule_dt.day == 1:
            unit.exist_summary =  [
                {
                    "worktime_summary": [0 for k in range(len(unit.main_shift.worktime_list))],
                    "area_summary": [0 for l in range(len(areas))]
                }
                for i in range(len(unit.staff_list))]
        else:
            # 当月第一天
            start_date_str = str(datetime.datetime(
                year=schedule_dt.year, 
                month=schedule_dt.month,
                day=1))

            # 排班日前一天
            end_date_str = str(datetime.datetime(
                year=schedule_dt.year, 
                month=schedule_dt.month,
                day=schedule_dt.day - 1))

            res_data, err_msg = get_worktime_by_range(start_date_str, end_date_str, is_data_ol)
            if err_msg:
                return None, err_msg

            exist_summary = parse_existing_workdata(
                res_data, unit)
            unit.exist_summary = exist_summary

        # 解析保障任务数据
        task_list = []
        for gua in unit.guarantee_list:
            task_list += parse_task_data(flight_dto, gua.id)
        task_list.sort(key=lambda t: t["startPlanTime"])

        unit.task_list =  task_list

    return {
        "schedule_demands": schedule_demands,
        "arrangeData": arrangeData,
        "flight_dto": flight_dto,
    }, None


def run_scheduling(schedule_demands, message_id,
        callback_url, result=None, is_nextday=False, flight_dto=[]):
    """
    用于异步调用，返回运算结果至指定地址
    """

    print("==========")
    receive_time = datetime.datetime.now()
    print(f"Optimization Start: {receive_time}")

    # 若未传入结果，则在异步方法内执行排班运算
    if not result:
        result = schedule_alg(
            schedule_demands=schedule_demands, 
            message_id=message_id, 
            is_nextday=is_nextday,
            flight_dto=flight_dto)

    finish_time = datetime.datetime.now()
    print(f"Optimization Finished: {finish_time}")
    print(f"Time Elapsed: {finish_time - receive_time}s")
    print()

    # 设置callback相关参数，发送请求

    # TODO: 处理排班失败的情况
    result = json.dumps(result)
    headers = {'Content-Type': 'application/json;charset=UTF-8'}

    print()
    print("===Callback Info===")
    print(f"CallbackURL: {callback_url}")
    print(f"Headers: {headers}")

    res = requests.post(url=callback_url, data=result, headers=headers)
   
    print(f"Response: {res}")
    print()


def schedule_alg(schedule_demands, message_id,
        is_nextday=False, flight_dto=[], save_plot=False):
    """
    测试阶段临时方法，将计算逻辑和异步调用的 run_scheduling 方法分离，避免后台运行算法难以调试。
    整合排班结果。
    """

    try:
        res_list = []
        if not is_nextday:
            # 分unit进行中长期排班计算
            for unit in schedule_demands["schedule_list"]:
                res = schedule_longterm(unit)
                res_list.append(res)
        else:
            # 次日排班计算
            for unit in schedule_demands["schedule_list"]:
                res = schedule_nextday(unit, flight_dto)
                res_list.append(res)

        # 整合结果
        result = res_list[0]
        if len(res_list) > 1:
            for i in range(1, len(res_list)):
                result["arrangeDto"] += res_list[i]["arrangeDto"]
        
        result["messageId"] = message_id

        return {"code": message.SUCCESS["resultCode"],
                "msg": message.SUCCESS["resultMessage"],
                "data": result}

    except Exception as ex:
        return {"code": message.SOLVER_ERROR["resultCode"],
                "msg": message.SOLVER_ERROR["resultMessage"],
                "data": ""}


def schedule_longterm(unit):
    """
    长期排班算法
    """

    # 建立模型
    schedule_solver = ScheduleSolver(
        start_date=unit.start_date,
        end_date=unit.end_date,
        staff_list=unit.staff_list,
        main_shift=unit.main_shift,
        aux_shift=unit.aux_shift,
        department=unit.department,
        guarantee_step_list=[],
        exist_summary=unit.exist_summary
    )

    if unit.main_shift:
        schedule_solver.solve_main()
        if schedule_solver.status == ScheduleSolver.MAIN_OPTIMAL:
            if not unit.aux_shift:
                schedule_solver.print_schedule_info()
            else:
                schedule_solver.solve_aux()  
                if schedule_solver.status == ScheduleSolver.AUX_OPTIMAL:
                    schedule_solver.print_schedule_info()

    result = schedule_solver.calc_return_data()

    # # 存储结果
    # with open(out_path, "w") as f:
    #     json.dump(result, f) 

    # # 图像绘制
    # workTimeId_list = []
    # for wt in schedule_solver._main_shift.worktime_list:
    #     workTimeId_list.append(wt.id)

    # if schedule_solver._aux_shift:
    #     for wt in schedule_solver._aux_shift.worktime_list:
    #         workTimeId_list.append(wt.id)

    # json2graph(filename = out_path, 
    #             startdate = schedule_solver._start_date, 
    #             enddate = schedule_solver._end_date, 
    #             shift_rule = workTimeId_list)

    return result


def schedule_nextday(unit, flight_dto):
    """
    次日排班算法
    """

    # 还原当日每个班型的工作员工列表
    # 班型列表
    worktime_list = unit.main_shift.worktime_list
    # 班型id列表，方便索引
    wt_ids = [wt.id for wt in worktime_list]
    # 当日每个班型的员工列表
    wt_staff_ids = [[] for wt in worktime_list]
    # 员工和arrangeDto的映射字典
    staff_to_arrange = {}

    for arr_index, arr in enumerate(unit.arrange_data):
        # 班型在班制里的index
        index = wt_ids.index(arr["workTimeId"])
        # 向相应班型的列表添加员工
        wt_staff_ids[index].append(arr["userId"])
        # 向相应员工-任务字典里添加数据
        staff_to_arrange[arr["userId"]] = arr_index

    # 对每个班型的员工分组、分区、安排加班
    for wt_staffs in wt_staff_ids:
        staff_cnt = len(wt_staffs)

        # 员工分区
        solver = AreaSolver(unit=unit, staff_ids=wt_staffs) 
        solver.solve()
        area_res = solver.calc_return_data()

        area_staffs = {}
        for area in unit.areas:
            area_staffs[area.id] = []

        for item in area_res:
            staff_id, area_id = item[0], item[1]
            unit.arrange_data[staff_to_arrange[staff_id]]["areaId"] = area_id
            area_staffs[area_id].append(staff_id)

        # 员工分组
        if int(unit.department.id) == 1587: # 客舱清洁
            gua = unit.guarantee_list[0]
            group_min = gua.group_min_employee
            group_max = gua.group_max_employee

            solver = GroupSolver(
                area_staffs=area_staffs, 
                flights=flight_dto,
                group_min=group_min,
                group_max=group_max
            )
            solver.solve()
            group_res = solver.calc_return_data()                

            if solver.status == GroupSolver.OPTIMAL:
                for item in group_res:
                    staff_id, group_id = item[0], item[1]
                    unit.arrange_data[staff_to_arrange[staff_id]]["groupId"] = group_id
        else:
            for i in range(staff_cnt):
                group_id = i
                unit.arrange_data[staff_to_arrange[wt_staffs[i]]]["groupId"] = group_id

    # 结果信息显示
    print("=====")
    print("Area/Group Result")
    for arr in unit.arrange_data:
        print(arr)
        print("--")
    print()
    
    # 员工加班
    # 资源预估算法，根据航班预估上班员工是否充足
    overtime_est_cnt = flight_density_analyze(unit, flight_dto)

    # TEST: 模拟计算加班人数
    unit.main_shift = None
    overtime_res = {"arrangeDto": []}

    for wt in unit.aux_shift.worktime_list:
        wt.required_staff_cnt = overtime_est_cnt

    overtime_res = schedule_overtime(unit) 

    # 结果信息显示
    print("=====")
    print("OverTime Result")
    for arr in overtime_res["arrangeDto"]:
        print(arr)
        print("--") 
    print()  

    # 整合结果
    result = {}
    result["appId"] = 1
    result["arrangeCount"] = len(unit.arrange_data)
    result["arrangeDto"] = unit.arrange_data
    result["eqIndex"] = 1
    result["messageId"] = None
    result["overTimeDto"] = {
        "reason": "航班保障人员不足",
        "number": len(overtime_res["arrangeDto"]),
        "arrangeDto": overtime_res["arrangeDto"]
    }
        
    return result


def schedule_overtime(unit):
    """
    加班排班算法（次日排班算法的一部分）
    """

    # 建立模型
    schedule_solver = ScheduleSolver(
        start_date=unit.start_date,
        end_date=unit.end_date,
        staff_list=unit.staff_list,
        main_shift=None,
        aux_shift=unit.aux_shift,
        department=unit.department,
        guarantee_step_list=[],
        exist_summary=unit.exist_summary,
        arrange_data=unit.arrange_data
    )

    schedule_solver.solve_next_day()
    result = schedule_solver.calc_return_data()
    return result

def flight_density_analyze(unit, flight_dto, work_task_ratio=1):
    #！注：需具体日期，以检查任务覆盖，8.26 0-24点上班的人员无法保障8.27 凌晨2点的任务
    # 航班密度统计的粒度
    time_fmt = "%Y-%m-%d %H:%M"
    granularity = 10

    schedule_date_start = datetime.datetime.strptime(
        unit.task_list[ 0]["startPlanTime"][:-5] + "00:00", 
        time_fmt)

    earliest = datetime.datetime.strptime(unit.task_list[ 0]["startPlanTime"], time_fmt)
    latest   = datetime.datetime.strptime(unit.task_list[-1]["endPlanTime"], time_fmt)
    earl_int = int((earliest - schedule_date_start).total_seconds()/60)
    late_int = int((latest - schedule_date_start).total_seconds()/60)
    earl_index = int(earl_int / granularity)
    late_index = int(late_int / granularity) + 1    

    day_minutes = 24 * 60
    task_density_data = [0] * max(late_index, math.ceil(day_minutes/granularity))
    schedule_density_data = [0] * max(late_index, math.ceil(day_minutes/granularity))
    more_employee_data = [0] * max(late_index, math.ceil(day_minutes/granularity))

    # 班制数据
    main_wt_indexes = []
    for wt in unit.main_shift.worktime_list:
        for sec in wt.time_section_list:
            s_int = sec.start_time.hour * 60 + sec.start_time.minute
            e_int = sec.end_time.hour * 60 + sec.end_time.minute + 1440 * sec.is_cross_day
            s_index = int(s_int / granularity)
            e_index = int(e_int / granularity) + 1
            main_wt_indexes += [i for i in range(s_index, e_index)]
    main_wt_indexes = list(set(main_wt_indexes))

    aux_wt_indexes = []
    for wt in unit.aux_shift.worktime_list:
        for sec in wt.time_section_list:
            s_int = sec.start_time.hour * 60 + sec.start_time.minute
            e_int = sec.end_time.hour * 60 + sec.end_time.minute + 1440 * sec.is_cross_day
            s_index = int(s_int / granularity)
            e_index = int(e_int / granularity) + 1
            aux_wt_indexes += [i for i in range(s_index, e_index)]
    aux_wt_indexes = list(set(aux_wt_indexes))

    all_wt_indexes = list(set(main_wt_indexes + aux_wt_indexes))

    # 测试：整体航班分析
    no_task_flights = []
    no_time_tasks = []
    not_covered_tasks = []      # 班制内班型无法覆盖任务的，进行错误提示。
    aux_not_covered_tasks = []  # 需要加班的时段，在班制副班时段外的，进行错误提示
    for fli in flight_dto:
        if not fli.raw_tasks:
            no_task_flights.append(fli)
            continue

        for task in fli.raw_tasks:
            if not task["startPlanTime"]:
                no_time_tasks.append(task)
                continue
            if not task["endPlanTime"]:
                task["endPlanTime"] = task["startPlanTime"]

    # 测试：保障环节任务分析
    for task in unit.task_list:
        if not task["startPlanTime"]:
            no_time_tasks.append(task)
            continue
        if not task["endPlanTime"]:
            task["endPlanTime"] = task["startPlanTime"]

        start_time_str = task["startPlanTime"]
        end_time_str = task["endPlanTime"]
        s_time = datetime.datetime.strptime(start_time_str, time_fmt)
        e_time = datetime.datetime.strptime(end_time_str, time_fmt)
        s_time_int = int((s_time - schedule_date_start).total_seconds()/60)
        e_time_int = int((e_time - schedule_date_start).total_seconds()/60)
        s_index = int(s_time_int / granularity)
        e_index = int(e_time_int / granularity) + 1
        # 班制与给定保障任务时段重合度检查
        for index in range(s_index, e_index):
            if index not in all_wt_indexes:
                not_covered_tasks.append(task)
            if index not in aux_wt_indexes:
                aux_not_covered_tasks.append(task)
            task_density_data[index] += 1

    print("===任务分布分析===")
    print(f"总航班数： {len(flight_dto)}")
    print(f"无任务航班： {len(no_task_flights)}趟")
    print(f"无时间任务： {len(no_time_tasks)}个")
    print()
    print(f"保障环节{unit.guarantee_list[0].name}任务数： {len(unit.task_list)}")
    print(f"班制无法保障的任务数： {len(not_covered_tasks)}")
    print(f"副班制无法保障的任务数（无法通过加班处理）： {len(aux_not_covered_tasks)}")
           
    # 上班人数分析
    normalwork_data = unit.arrange_data
    overtime_data = unit.overtime_data

    for d in normalwork_data + overtime_data:
        s_time = datetime.datetime.strptime(d["startTime"], "%Y-%m-%d %H:%M:%S")
        e_time = datetime.datetime.strptime(d["endTime"], "%Y-%m-%d %H:%M:%S")
        s_time_int = int((s_time - schedule_date_start).total_seconds()/60)
        e_time_int = int((e_time - schedule_date_start).total_seconds()/60)
        s_index = int(s_time_int / granularity)
        e_index = int(e_time_int / granularity) + 1

        for i in range(s_index, e_index):
            schedule_density_data[i] += 1

    # 任务分布和人员上班时间分布对比，决定加班人数
    for i in range(len(schedule_density_data)):
        diff = math.ceil(task_density_data[i] / work_task_ratio) - schedule_density_data[i]
        if diff > 0 and (i in aux_wt_indexes):
            more_employee_data[i] = diff

    return max(more_employee_data)